<?php
/**
 * @author 595949289@qq.com
 * @date 2024/12/31 9:05
 */

namespace CuiFox\yii\components;

use Yii;
use yii\caching\TagDependency;
use yii\di\Instance;
use yii\base\Component;
use yii\db\ActiveRecord;
use CuiFox\yii\rbac\Route;
use yii\base\InvalidConfigException;

class Menu extends Component
{
    /**
     * @var ActiveRecord
     */
    public $modelClass;

    /**
     * @var yii\caching\Cache|array|string|false the user object representing the authentication status or the ID of the user application component.
     */
    public $cache = 'cache';

    public $cachePrefix = 'menu';

    public $appId;

    private $tag;

    /**
     * @throws InvalidConfigException
     */
    public function init()
    {
        parent::init();

        if ($this->cache !== false) {
            $this->cache = Instance::ensure($this->cache, 'yii\caching\Cache');
        }

        if ($this->appId == null) {
            $this->appId = Yii::$app->id;
        }

        if ($this->modelClass === null) {
            throw new InvalidConfigException('Menu::modelClass must be set.');
        }

        $this->tag = $this->cachePrefix . $this->appId;
    }

    /**
     * @param $userId
     * @param null $root
     * @param null $callback
     * @param bool $refresh
     * @return mixed
     */
    public function getMenu($userId, $root = null, $callback = null, $refresh = false)
    {
        if ($refresh) {
            $this->invalidate();
        }

        $key = [$this->cachePrefix, $this->appId, $userId, $root];
        return $this->cache->getOrSet($key, function () use ($userId, $root, $callback) {
            $filtered = $this->filter(self::getAssignedRoute($userId));
            $menus = $this->modelClass::find()->where(['app_id' => $this->appId])->asArray()->indexBy('id')->all();
            $assigned = self::requiredParent($filtered, $menus);
            return self::normalizeMenu($assigned, $menus, $callback, $root);
        }, null, new TagDependency(['tags' => $this->tag]));
    }

    /**
     * @param $routes
     * @return array
     */
    private function filter($routes)
    {
        $prefix = '\\';
        $filter1 = $filter2 = [];

        foreach ($routes as $route) {
            if (strpos($route, $prefix) !== 0) {
                if (substr($route, -1) === '/') {
                    $prefix = $route;
                    $filter1[] = $route . '%';
                } else {
                    $filter2[] = $route;
                }
            }
        }

        $filtered = [];
        $query = $this->modelClass::find()->select(['id'])->where(['app_id' => $this->appId])->asArray();
        if (count($filter2)) {
            $filtered = $query->andWhere(['route' => $filter2])->column();
        }
        if (count($filter1)) {
            $query->andWhere('route like :filter');
            foreach ($filter1 as $filter) {
                $filtered = array_merge($filtered, $query->params([':filter' => $filter])->column());
            }
        }

        return $filtered;
    }

    /**
     * @param $userId
     * @return array
     */
    public static function getAssignedRoute($userId)
    {
        /**
         * @var $manager \yii\rbac\BaseManager
         */
        $manager = Yii::$app->getAuthManager();
        $routes = [];
        if ($userId !== null) {
            foreach ($manager->getPermissionsByUser($userId) as $name => $value) {
                if ($name[0] === Route::PREFIX) {
                    if (substr($name, -2) === '/*') {
                        $name = substr($name, 0, -1);
                    }
                    $routes[] = $name;
                }
            }
        }
        foreach ($manager->defaultRoles as $role) {
            foreach ($manager->getPermissionsByRole($role) as $name => $value) {
                if ($name[0] === Route::PREFIX) {
                    if (substr($name, -2) === '/*') {
                        $name = substr($name, 0, -1);
                    }
                    $routes[] = $name;
                }
            }
        }
        $routes = array_unique($routes);
        sort($routes);
        return $routes;
    }

    /**
     * @param $assigned
     * @param $menus
     * @return mixed
     */
    private static function requiredParent($assigned, &$menus)
    {
        $count = count($assigned);
        for ($i = 0; $i < $count; $i++) {
            $id = $assigned[$i];
            $parent_id = $menus[$id]['parent'];
            if ($parent_id !== null && !in_array($parent_id, $assigned)) {
                $assigned[$count++] = $parent_id;
            }
        }
        return $assigned;
    }

    /**
     * @param $route
     * @return array|string
     */
    public static function parseRoute($route)
    {
        if (!empty($route)) {
            $url = [];
            $r = explode('?', $route);
            $url[0] = $r[0];
            $parts = isset($r[1]) ? explode('&', $r[1]) : [];
            foreach ($parts as $part) {
                $part = explode('=', $part);
                $url[$part[0]] = isset($part[1]) ? $part[1] : '';
            }
            unset($r);
            return $url;
        }
        return '#';
    }

    /**
     * @param $assigned
     * @param $menus
     * @param $callback
     * @param null $parent
     * @return array
     */
    private static function normalizeMenu(&$assigned, &$menus, $callback, $parent = null)
    {
        $result = [];
        $order = [];
        foreach ($assigned as $id) {
            $menu = $menus[$id];
            if ($menu['parent'] == $parent) {
                $menu['children'] = self::normalizeMenu($assigned, $menus, $callback, $id);
                if ($callback !== null) {
                    $item = call_user_func($callback, $menu);
                } else {
                    $item = [
                        'label' => $menu['name'],
                        'url' => self::parseRoute($menu['route']),
                    ];
                    if ($menu['children'] != []) {
                        $item['items'] = $menu['children'];
                    }
                }
                $result[] = $item;
                $order[] = $menu['order'];
            }
        }

        if ($result != []) {
            array_multisort($order, $result);
        }

        return $result;
    }

    /**
     * Cache invalidate
     */
    public function invalidate()
    {
        TagDependency::invalidate($this->cache, $this->tag);
    }
}